﻿using System;
using System.Collections;
using System.Reflection;

namespace Complaint.Common
{
    public static class ReflectionHelper
    {
        #region Declarations

        private enum Mode
        {
            Get = 0,
            Set = 1
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Drill down the entire object graph based on the specified nesting base type
        /// to copy property values from source object to target object.
        /// </summary>
        /// <param name="sourceOject">source object</param>
        /// <param name="targetObject">target object</param>
        /// <param name="nestingType">nesting type to drill down</param>
        public static void CopyPropertyValues(
            Object sourceOject,
            Object targetObject,
            Type nestingType)
        {
            var sourceProperties = sourceOject.GetType().GetProperties();
            foreach (var p in sourceProperties)
            {
                var targetProperty = targetObject.GetType().GetProperty(p.Name);
                if (p.PropertyType.IsArray && targetProperty.PropertyType.IsArray)
                {
                    CopyArrayValues(sourceOject, targetObject, p, targetProperty, nestingType);
                }
                else
                {
                    var sourcePropertyValue = p.GetValue(sourceOject, null);
                    if (targetProperty.PropertyType.IsSubclassOf(nestingType))
                    {
                        var targetValue = Activator.CreateInstance(targetProperty.PropertyType);
                        CopyPropertyValues(sourcePropertyValue, targetValue, nestingType);
                        SetValue(targetObject, p.Name, targetValue);
                    }
                    else
                    {
                        SetValue(targetObject, p.Name, sourcePropertyValue);
                    }
                }
            }
        }

        /// <summary>
        /// use this method to set value by reflection
        /// </summary>
        /// <param name="parentObject"></param>
        /// <param name="targetProperty"></param>
        /// <param name="targetValue"></param>
        public static void SetValue(Object parentObject, string targetProperty, Object targetValue)
        {
            string[] props = targetProperty.Split('.');
            Object nextObject = Process(parentObject, targetProperty, Mode.Set);

            PropertyInfo pInfo = nextObject.GetType().GetProperty(props[props.Length - 1]);

            if (pInfo == null) return;

            GenericSetValue(targetValue, nextObject, pInfo);
        }

        /// <summary>
        /// Use this method to get value by reflection
        /// </summary>
        /// <param name="parentObject"></param>
        /// <param name="targetProperty"></param>
        /// <returns></returns>
        public static Object GetValue(Object parentObject, string targetProperty)
        {
            string[] props = targetProperty.Split('.');
            Object nextObject = Process(parentObject, targetProperty, Mode.Get);

            if (nextObject == null) return null;

            PropertyInfo pInfo = nextObject.GetType().GetProperty(props[props.Length - 1]);
            return pInfo.GetValue(nextObject, null);
        }

        #endregion

        #region Private Methods

        private static void CopyArrayValues(
            object sourceOject,
            object targetObject,
            PropertyInfo sourceProperty,
            PropertyInfo targetProperty,
            Type nestingType)
        {
            var sourcePropertyValue = sourceProperty.GetValue(sourceOject, null) as Array;
            if (sourcePropertyValue != null)
            {
                var targetElementType = targetProperty.PropertyType.GetElementType();
                var length = sourcePropertyValue.Length;
                var targetValue =
                    Array.CreateInstance(
                        targetElementType,
                        length);
                if (targetElementType.IsSubclassOf(nestingType))
                {
                    for (var i = targetValue.GetLowerBound(0); i <= targetValue.GetUpperBound(0); i++)
                    {
                        var element =
                            Activator.CreateInstance(targetElementType);
                        CopyPropertyValues(sourcePropertyValue.GetValue(i), element, nestingType);
                        targetValue.SetValue(element, i);
                    }
                }
                else
                {
                    for (var i = targetValue.GetLowerBound(0); i <= targetValue.GetUpperBound(0); i++)
                        targetValue.SetValue(sourcePropertyValue.GetValue(i), i);
                }
                SetValue(targetObject, sourceProperty.Name, targetValue);
            }
        }

        private static Object Process(Object parentObject, string targetProperty, Mode callingMode)
        {
            if (parentObject == null) return null;

            Type parentType = parentObject.GetType();
            Object nextObject = parentObject;

            string[] props = targetProperty.Split('.');

            PropertyInfo pInfo = parentType.GetProperty(GetPropertyName(props[0]));

            if (props.Length > 1)
            {
                nextObject =
                    pInfo.PropertyType.IsArray ?
                    ProcessArray(pInfo, parentObject, targetProperty, callingMode) :
                    ProcessObject(pInfo, parentObject, callingMode);
            }

            if (props.Length > 2) nextObject = Process(nextObject, targetProperty.Substring(targetProperty.IndexOf('.') + 1), callingMode);

            return nextObject;
        }

        private static object ProcessObject(PropertyInfo pInfo, object parentObject, Mode callingMode)
        {
            Object workingObject =
                pInfo.GetValue(parentObject, null) ?? Activator.CreateInstance(pInfo.PropertyType);

            if (callingMode == Mode.Set) pInfo.SetValue(parentObject, workingObject, null);

            return workingObject;
        }

        private static Object ProcessArray(PropertyInfo pInfo, Object parentObject, string targetProperty, Mode callingMode)
        {
            int currentItemIndex = GetCurrentIndex(targetProperty);
            Object workingObject = pInfo.GetValue(parentObject, null);

            if (callingMode == Mode.Set)
                return SetArrayItem(pInfo, parentObject, currentItemIndex, workingObject);
            return GetArrayItem(currentItemIndex, workingObject);
        }

        private static object GetArrayItem(int currentItemIndex, Object workingObject)
        {
            var currentArray = workingObject as Array;
            if (currentArray != null)
            {
                if (currentItemIndex >= currentArray.Length) return null;
                return currentArray.GetValue(currentItemIndex);
            }
            return null;
        }

        private static object SetArrayItem(PropertyInfo pInfo, Object parentObject, int currentItemIndex, Object workingObject)
        {
            var refDataArray = new ArrayList();

            if (workingObject != null)
            {
                var currentArray = workingObject as Array;

                if (currentArray != null)
                {
                    if (currentArray.Length >= currentItemIndex + 1)
                    {
                        return currentArray.GetValue(currentItemIndex);
                    }

                    for (int i = 0; i < currentArray.Length; i++)
                    {
                        refDataArray.Add(currentArray.GetValue(i));
                    }
                }
            }

            Type objectType = GetType(pInfo.PropertyType.FullName.Substring(0, pInfo.PropertyType.FullName.Length - 2));
            object item = Activator.CreateInstance(objectType);

            refDataArray.Add(item);

            workingObject = refDataArray.ToArray(item.GetType());
            pInfo.SetValue(parentObject, workingObject, null);

            return item;
        }

        public static Type GetType(string typename)
        {
            Type type = null;
            foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                type = assembly.GetType(typename, false);
                if (type != null) return type;
            }
            return type;
        }

        private static int GetCurrentIndex(string targetProperty)
        {
            int startPos = targetProperty.IndexOf('[');
            int endPos = targetProperty.IndexOf(']');
            int len = endPos - startPos - 1;

            if (len < 1) return 0;

            return Convert.ToInt32(targetProperty.Substring(startPos + 1, len));
        }

        private static string GetPropertyName(string prop)
        {
            if (prop.IndexOf('[') == -1)
                return prop;
            return prop.Substring(0, prop.IndexOf('['));
        }

        private static void GenericSetValue(Object targetValue, Object nextObject, PropertyInfo pInfo)
        {
            if (pInfo.PropertyType.IsEnum)
                pInfo.SetValue(nextObject, Enum.Parse(pInfo.PropertyType, targetValue.ToString()), null);
            else
                pInfo.SetValue(nextObject, targetValue, null);
        }

        #endregion
    }
}
